
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import pandas as pd
import traceback
import matplotlib
matplotlib.use('TkAgg')  # 确保使用正确的后端
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.ticker import MaxNLocator
import seaborn as sns
from datetime import datetime

class StudentAnalysisApp:
    def __init__(self, root):
        self.root = root
        self.root.title("学生成绩分析系统")
        self.root.geometry("1200x850")

        # 设置全局异常处理
        self.root.report_callback_exception = self.handle_exception

        # 解决中文乱码问题
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 使用黑体
        plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

        self.create_widgets()
        self.student_data = None
        self.subject_columns = []
        self.exam_dates = []
        self.current_figure = None  # 存储当前图表对象
        self.subject_colors = {}  # 存储学科颜色映射
        self.current_student_ranks = None  # 存储当前学生排名数据
        self.subject_vars = {}  # 存储科目选择状态

    def handle_exception(self, exc_type, exc_value, exc_traceback):
        """统一处理Tkinter回调异常"""
        error_msg = f"发生错误: {exc_value}\n\n详细追踪:\n{traceback.format_exc()}"
        messagebox.showerror("系统错误", error_msg)

    def create_widgets(self):
        """创建界面组件"""
        # 文件选择区域
        file_frame = ttk.LabelFrame(self.root, text="数据文件")
        file_frame.pack(fill="x", padx=10, pady=10)

        self.file_path = tk.StringVar()
        ttk.Entry(file_frame, textvariable=self.file_path, width=70).pack(side="left", padx=5, pady=5)
        ttk.Button(file_frame, text="浏览", command=self.browse_file).pack(side="left", padx=5)

        # 模板下载区域
        template_frame = ttk.LabelFrame(self.root, text="模板下载")
        template_frame.pack(fill="x", padx=10, pady=5)

        ttk.Button(template_frame, text="下载模板", command=self.download_template).pack(side="left", padx=5)
        ttk.Label(template_frame, text="下载标准数据模板文件").pack(side="left", padx=5)

        # 学生学号输入区域
        student_frame = ttk.LabelFrame(self.root, text="学生信息")
        student_frame.pack(fill="x", padx=10, pady=5)

        ttk.Label(student_frame, text="学生学号:").pack(side="left", padx=10)
        self.student_id = tk.StringVar()
        ttk.Entry(student_frame, textvariable=self.student_id, width=15).pack(side="left", padx=5)

        # 添加绘制按钮
        ttk.Button(student_frame, text="执行分析", command=self.analyze).pack(side="left", padx=5)
        ttk.Button(student_frame, text="绘制排名曲线", command=self.plot_rank_curves).pack(side="left", padx=5)

        # 结果显示区域
        result_frame = ttk.LabelFrame(self.root, text="分析结果")
        result_frame.pack(fill="both", expand=True, padx=10, pady=5)

        # 创建结果标签页
        self.notebook = ttk.Notebook(result_frame)
        self.notebook.pack(fill="both", expand=True, padx=5, pady=5)

        # 文本结果标签页
        self.text_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.text_tab, text="文本结果")

        self.result_text = tk.Text(self.text_tab, wrap="word", font=("Microsoft YaHei", 10))
        self.result_text.pack(fill="both", expand=True, padx=5, pady=5)

        scrollbar = ttk.Scrollbar(self.text_tab, command=self.result_text.yview)
        scrollbar.pack(side="right", fill="y")
        self.result_text.config(yscrollcommand=scrollbar.set)

        # 图表结果标签页
        self.chart_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.chart_tab, text="排名曲线")

        # 科目选择区域
        self.subject_frame = ttk.LabelFrame(self.chart_tab, text="选择绘制科目")
        self.subject_frame.pack(fill="x", padx=10, pady=5)

        # 图表容器
        self.chart_frame = ttk.Frame(self.chart_tab)
        self.chart_frame.pack(fill="both", expand=True, padx=10, pady=10)

        # 退出按钮区域
        exit_frame = ttk.Frame(self.root)
        exit_frame.pack(fill="x", padx=10, pady=10)
        ttk.Button(exit_frame, text="退出程序", command=self.exit_program,
                   style="Exit.TButton").pack(side="right", padx=5)

        # 设置退出按钮样式
        style = ttk.Style()
        style.configure("Exit.TButton", foreground="white", background="#e74c3c",
                        font=("Microsoft YaHei", 10, "bold"), padding=10)

    def browse_file(self):
        """选择数据文件"""
        file_path = filedialog.askopenfilename(
            filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")]
        )
        if file_path:
            self.file_path.set(file_path)
            self.load_data()

    def download_template(self):
        """下载模板文件并让用户选择保存位置"""
        try:
            # 创建模板数据
            template_data = {
                "学号": ["S001", "S001", "S002", "S002"],
                "姓名": ["张三", "张三", "李四", "李四"],
                "考试时间": ["2025-01-15", "2025-03-20", "2025-01-15", "2025-03-20"],
                "语文": [85, 88, 92, 95],
                "数学": [90, 92, 88, 85],
                "英语": [82, 85, 85, 88],
                "物理": [75, 78, 80, 82],
                "化学": [88, 85, 85, 88],
                "生物": [92, 90, 78, 82],
                "地理": [85, 88, 90, 85],
                "政治": [90, 85, 88, 92]
            }

            template_df = pd.DataFrame(template_data)

            # 让用户选择保存位置
            file_path = filedialog.asksaveasfilename(
                defaultextension=".xlsx",
                filetypes=[("Excel文件", "*.xlsx")],
                title="保存模板文件"
            )

            if not file_path:  # 用户取消了保存
                return

            template_df.to_excel(file_path, index=False)
            messagebox.showinfo("下载模板", f"模板文件已保存到:\n{file_path}")
        except Exception as e:
            messagebox.showerror("错误", f"创建模板失败: {str(e)}")

    def load_data(self):
        """加载学生数据文件"""
        file_path = self.file_path.get()
        if not file_path:
            messagebox.showwarning("警告", "请先选择数据文件")
            return

        try:
            # 根据文件类型选择读取方式
            if file_path.endswith('.xlsx'):
                self.student_data = pd.read_excel(file_path)
            elif file_path.endswith('.csv'):
                self.student_data = pd.read_csv(file_path)
            else:
                messagebox.showerror("错误", "不支持的文件格式")
                return

            # 检查必要列是否存在
            if '学号' not in self.student_data.columns or '考试时间' not in self.student_data.columns:
                messagebox.showerror("错误", "数据文件必须包含'学号'和'考试时间'列")
                return

            # 改进的科目检测逻辑
            self.subject_columns = self.detect_subject_columns()

            if not self.subject_columns:
                messagebox.showwarning("警告", "未检测到科目列，请手动添加或检查数据格式")
            else:
                # 获取考试时间列表
                self.exam_dates = sorted(self.student_data['考试时间'].unique())

                # 为每个学科分配颜色
                self.assign_subject_colors()

                messagebox.showinfo("成功",
                                    f"成功加载数据，共{len(self.student_data)}条记录\n"
                                    f"检测到科目: {', '.join(self.subject_columns)}\n"
                                    f"考试时间: {', '.join(map(str, self.exam_dates))}")

        except FileNotFoundError:
            messagebox.showerror("错误", "文件不存在，请检查路径")
        except pd.errors.EmptyDataError:
            messagebox.showerror("错误", "文件为空或格式不正确")
        except Exception as e:
            messagebox.showerror("错误", f"加载文件失败: {str(e)}")

    def detect_subject_columns(self):
        """检测科目列 - 更智能的检测逻辑"""
        if self.student_data is None:
            return []

        # 常见科目关键词（包括新增的地理和政治）
        subject_keywords = ["语文", "数学", "英语", "物理", "化学", "生物",
                            "历史", "地理", "政治"]

        # 排除非科目列
        exclude_columns = ["学号", "姓名", "班级", "年级", "考试时间"]

        # 检测科目列
        subject_columns = []
        for col in self.student_data.columns:
            # 排除已知的非科目列
            if col in exclude_columns:
                continue

            # 检查是否包含科目关键词
            if any(keyword in col for keyword in subject_keywords):
                subject_columns.append(col)

        return subject_columns

    def assign_subject_colors(self):
        """为每个学科分配唯一的颜色"""
        if not self.subject_columns:
            return

        # 使用seaborn的颜色调色板
        colors = sns.color_palette("husl", len(self.subject_columns) + 1)

        # 为总分分配特殊颜色（红色）
        self.subject_colors["总分排名"] = "#FF0000"

        # 为每个学科分配颜色
        for i, subject in enumerate(self.subject_columns):
            # 将RGB元组转换为十六进制颜色代码
            r, g, b = colors[i]
            hex_color = "#{:02x}{:02x}{:02x}".format(int(r*255), int(g*255), int(b*255))
            self.subject_colors[subject + "排名"] = hex_color

    def create_subject_selection(self):
        """创建科目选择区域"""
        # 清除现有的选择控件
        for widget in self.subject_frame.winfo_children():
            widget.destroy()

        # 初始化科目选择变量
        self.subject_vars = {}

        # 添加总分选择框
        total_var = tk.BooleanVar(value=True)
        self.subject_vars["总分排名"] = total_var
        ttk.Checkbutton(
            self.subject_frame,
            text="总分排名",
            variable=total_var,
            onvalue=True,
            offvalue=False
        ).pack(side="left", padx=10, pady=5)

        # 添加各科目选择框
        for subject in self.subject_columns:
            subject_key = subject + "排名"
            subject_var = tk.BooleanVar(value=False)
            self.subject_vars[subject_key] = subject_var

            ttk.Checkbutton(
                self.subject_frame,
                text=subject_key,
                variable=subject_var,
                onvalue=True,
                offvalue=False
            ).pack(side="left", padx=10, pady=5)

    def analyze(self):
        """执行分析操作"""
        if self.student_data is None:
            messagebox.showwarning("警告", "请先加载数据")
            return

        student_id = self.student_id.get().strip()
        if not student_id:
            messagebox.showwarning("警告", "请输入学生学号")
            return

        self.result_text.delete(1.0, tk.END)

        try:
            self.analyze_single_student(student_id)
        except Exception as e:
            self.handle_exception(type(e), e, e.__traceback__)

    def analyze_single_student(self, student_id):
        """分析单个学生情况"""
        # 查找学生数据
        if '学号' not in self.student_data.columns:
            self.result_text.insert(tk.END, "数据中缺少'学号'列，无法查找学生\n", "warning")
            return

        student_data = self.student_data[self.student_data['学号'] == student_id]

        if student_data.empty:
            # 提供所有学号列表供参考
            all_ids = self.student_data['学号'].unique()[:10]  # 最多显示10个学号
            self.result_text.insert(tk.END,
                                    f"未找到学号为 {student_id} 的学生\n\n"
                                    f"数据中包含的学号示例: {', '.join(map(str, all_ids))}\n"
                                    f"共 {len(self.student_data)} 名学生\n", "warning")
            return

        student_name = student_data['姓名'].iloc[0]
        self.result_text.insert(tk.END, f"=== {student_name} ({student_id}) 成绩分析 ===\n\n", "title")

        # 按考试时间排序
        student_data = student_data.sort_values('考试时间')

        # 准备存储历次考试信息的列表
        exam_records = []
        subject_rank_history = {subject: [] for subject in self.subject_columns}

        # 计算每次考试的排名
        for date in student_data['考试时间'].unique():
            exam_data = self.student_data[self.student_data['考试时间'] == date]
            exam_data = exam_data.copy()

            # 计算总分
            exam_data['总分'] = exam_data[self.subject_columns].sum(axis=1)

            # 计算总分排名（使用中国式排名处理相同分数）
            exam_data['总分排名'] = exam_data['总分'].rank(method='min', ascending=False).astype(int)

            # 计算各科排名（使用中国式排名处理相同分数）
            for subject in self.subject_columns:
                exam_data[f'{subject}排名'] = exam_data[subject].rank(method='min', ascending=False).astype(int)

            student_exam_data = exam_data[exam_data['学号'] == student_id]
            if not student_exam_data.empty:
                student_row = student_exam_data.iloc[0]
                exam_records.append({
                    '考试时间': date,
                    '总分': student_row['总分'],
                    '总分排名': int(student_row['总分排名'])
                })

                # 记录各科排名
                for subject in self.subject_columns:
                    subject_rank_history[subject].append(int(student_row[f'{subject}排名']))

        # 转换为DataFrame
        rank_df = pd.DataFrame(exam_records)

        # 存储当前学生排名数据用于绘图
        self.current_student_ranks = {
            'exam_dates': rank_df['考试时间'].tolist(),
            'total_ranks': rank_df['总分排名'].tolist(),
            'subject_ranks': subject_rank_history,
            'student_name': student_name,
            'student_id': student_id
        }

        # 创建科目选择区域
        self.create_subject_selection()

        # 显示历次考试成绩和排名
        self.result_text.insert(tk.END, "历次考试成绩与排名:\n", "subtitle")
        self.result_text.insert(tk.END, rank_df.to_string(index=False))
        self.result_text.insert(tk.END, "\n\n")

        # 获取最近一次考试
        latest_exam_date = rank_df['考试时间'].max()
        latest_exam_data = self.student_data[self.student_data['考试时间'] == latest_exam_date]
        latest_exam_data = latest_exam_data.copy()

        # 计算最近一次考试的各科排名
        subject_ranks = {}
        total_students = len(latest_exam_data)
        for subject in self.subject_columns:
            # 计算该科目的排名
            subject_scores = latest_exam_data[subject]
            student_score = student_data[student_data['考试时间'] == latest_exam_date][subject].values[0]

            # 计算排名（排名数字越小表示越好）
            rank = (subject_scores > student_score).sum() + 1
            subject_ranks[subject] = {
                'score': student_score,
                'rank': rank
            }

        # 显示最近一次考试各科成绩和排名
        self.result_text.insert(tk.END, f"【{latest_exam_date} 各科成绩与排名】\n", "subtitle")
        for subject, data in subject_ranks.items():
            self.result_text.insert(tk.END,
                                    f"{subject}: {data['score']}分 (排名: {data['rank']}/{total_students})\n")
        self.result_text.insert(tk.END, "\n")

        # 找出优势学科和劣势学科
        strong_subjects = []
        weak_subjects = []

        # 优势学科：排名前5名
        for subject, data in subject_ranks.items():
            if data['rank'] <= 5:
                strong_subjects.append((subject, data['rank']))

        # 劣势学科：排名后5名
        for subject, data in subject_ranks.items():
            if data['rank'] > total_students - 5:
                weak_subjects.append((subject, data['rank']))

        # 特殊情况处理
        # 如果所有学科都排在前5名，则所有学科都是优势学科
        if len(strong_subjects) == len(self.subject_columns):
            # 所有学科都是优势学科，没有劣势学科
            weak_subjects = []

        # 如果所有学科都排在后5名，则只列出名次最差的2个学科
        if len(weak_subjects) == len(self.subject_columns):
            # 按排名降序排序（排名越大表示越差）
            weak_subjects.sort(key=lambda x: x[1], reverse=True)
            weak_subjects = weak_subjects[:2]  # 只取最差的2个学科

        # 显示优势学科
        if strong_subjects:
            self.result_text.insert(tk.END, "优势学科:\n", "strong")
            for subject, rank in strong_subjects:
                self.result_text.insert(tk.END, f"{subject} (排名: {rank})\n")
        else:
            self.result_text.insert(tk.END, "优势学科: 无\n", "strong")

        self.result_text.insert(tk.END, "\n")

        # 显示劣势学科
        if weak_subjects:
            self.result_text.insert(tk.END, "劣势学科:\n", "weak")
            for subject, rank in weak_subjects:
                self.result_text.insert(tk.END, f"{subject} (排名: {rank})\n")
        else:
            self.result_text.insert(tk.END, "劣势学科: 无\n", "weak")

        self.result_text.insert(tk.END, "\n")

        # 总体评价（只输出一个最重要的评价）
        self.result_text.insert(tk.END, "【总体评价】\n", "title")

        # 1. 检查是否所有学科都是优势学科
        if len(strong_subjects) == len(self.subject_columns):
            self.result_text.insert(tk.END, "所有学科均为优势学科，表现卓越！\n", "strong")
            return

        # 2. 检查是否有优势学科且没有劣势学科
        if strong_subjects and not weak_subjects:
            self.result_text.insert(tk.END, "在优势学科上表现突出，继续保持！\n", "strong")
            return

        # 3. 检查是否有劣势学科且没有优势学科
        if weak_subjects and not strong_subjects:
            weak_subjects_str = ", ".join([f"{sub}" for sub, _ in weak_subjects])
            self.result_text.insert(tk.END, f"在{weak_subjects_str}学科上需要加强！\n", "weak")
            return

        # 4. 检查是否既有优势学科又有劣势学科
        if strong_subjects and weak_subjects:
            weak_subjects_str = ", ".join([f"{sub}" for sub, _ in weak_subjects])
            self.result_text.insert(tk.END, f"学科表现不均衡，优势学科突出但{weak_subjects_str}需要加强！\n", "warning")
            return

        # 5. 其他情况（既没有优势学科也没有劣势学科）
        self.result_text.insert(tk.END, "各学科表现均衡，继续努力！\n", "normal")

    def plot_rank_curves(self):
        """绘制排名变化曲线"""
        if not self.current_student_ranks:
            messagebox.showwarning("警告", "请先执行分析获取学生数据")
            return

        # 清除图表容器中的旧内容
        for widget in self.chart_frame.winfo_children():
            widget.destroy()

        # 获取学生排名数据
        exam_dates = self.current_student_ranks['exam_dates']
        total_ranks = self.current_student_ranks['total_ranks']
        subject_ranks_dict = self.current_student_ranks['subject_ranks']
        student_name = self.current_student_ranks['student_name']
        student_id = self.current_student_ranks['student_id']

        # 创建图表
        fig, ax = plt.subplots(figsize=(10, 6))
        fig.subplots_adjust(bottom=0.2)  # 为底部标签留出空间

        # 获取用户选择的科目
        selected_subjects = [subject for subject, var in self.subject_vars.items() if var.get()]

        if not selected_subjects:
            messagebox.showinfo("提示", "请至少选择一个科目进行绘制")
            return

        # 绘制总分排名曲线（如果被选中）
        if "总分排名" in selected_subjects:
            ax.plot(exam_dates, total_ranks,
                    marker='o', color=self.subject_colors["总分排名"],
                    linewidth=3, markersize=8, label='总分排名', zorder=10)

            # 添加数据标签（显示具体排名）
            for i, rank in enumerate(total_ranks):
                ax.text(exam_dates[i], rank, str(rank),
                        ha='center', va='bottom', fontsize=10, color='black', zorder=11)

        # 绘制各学科排名曲线（如果被选中）
        for subject, ranks in subject_ranks_dict.items():
            subject_key = subject + "排名"
            if subject_key in selected_subjects:
                ax.plot(exam_dates, ranks,
                        marker='s', color=self.subject_colors[subject_key],
                        linewidth=2, markersize=6, label=f"{subject_key}")

                # 添加数据标签（显示具体排名）
                for i, rank in enumerate(ranks):
                    ax.text(exam_dates[i], rank, str(rank),
                            ha='center', va='bottom', fontsize=9, color='black', zorder=11)

        # 设置图表标题和标签
        ax.set_title(f"{student_name} ({student_id}) 排名变化曲线", fontsize=14)
        ax.set_xlabel("考试时间", fontsize=12)
        ax.set_ylabel("排名", fontsize=12)

        # 反转纵坐标轴（排名越小越好，显示在顶部）
        ax.invert_yaxis()

        # 添加网格
        ax.grid(True, linestyle='--', alpha=0.7)

        # 添加图例（放在图表外部）
        ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15),
                  ncol=3, fancybox=True, shadow=True)

        # 自动调整x轴标签角度
        plt.xticks(rotation=45, ha='right')

        # 设置y轴刻度为整数
        ax.yaxis.set_major_locator(MaxNLocator(integer=True))

        # 将图表嵌入到Tkinter界面
        canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
        canvas.draw()
        canvas.get_tk_widget().pack(fill="both", expand=True)

        # 存储当前图表对象
        self.current_figure = fig

        # 切换到图表标签页
        self.notebook.select(self.chart_tab)

    def exit_program(self):
        """退出程序"""
        if messagebox.askokcancel("退出", "确定要退出程序吗？"):
            self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = StudentAnalysisApp(root)

    # 设置文本样式
    app.result_text.tag_configure("title", foreground="#2c3e50", font=("Microsoft YaHei", 12, "bold"))
    app.result_text.tag_configure("subtitle", foreground="#3498db", font=("Microsoft YaHei", 10, "bold"))
    app.result_text.tag_configure("strong", foreground="#27ae60")
    app.result_text.tag_configure("weak", foreground="#e74c3c")
    app.result_text.tag_configure("warning", foreground="#f39c12")
    app.result_text.tag_configure("normal", foreground="#3498db")

    root.mainloop()
